package worker

import (
	"go.etcd.io/etcd/clientv3"
	"context"
	"../common"
)

//分布式锁(TXN事务)
type JobLock struct {
	//etcd客户端
	kv    clientv3.KV
	lease clientv3.Lease //租约,自动过期,避免宕机后,这个锁一直被占用

	jobName    string             //任务名(需要上锁的任务名)
	cancelFunc context.CancelFunc //用于中止自动续租
	leaseId    clientv3.LeaseID   //租约id
	isLocked   bool               //是否上锁成功
}

//初始化一把锁
func InitJobLock(jobName string, kv clientv3.KV, lease clientv3.Lease) (jobLock *JobLock) {
	jobLock = &JobLock{
		kv:      kv,
		lease:   lease,
		jobName: jobName,
	}
	return
}

//尝试上锁
func (jobLock *JobLock) TryLock() (err error) {
	var (
		leaseGrantResp *clientv3.LeaseGrantResponse
		cancelCtx      context.Context
		cancelFunc     context.CancelFunc
		leaseId        clientv3.LeaseID
		keepRespChan   <-chan *clientv3.LeaseKeepAliveResponse
		txn            clientv3.Txn
		lockKey        string
		txnResp        *clientv3.TxnResponse
	)
	//1,创建租约(5s)
	if leaseGrantResp, err = jobLock.lease.Grant(context.TODO(), 5); err != nil {
		return
	}

	//context用于自动取消续租
	cancelCtx, cancelFunc = context.WithCancel(context.TODO())

	//租约id
	leaseId = leaseGrantResp.ID

	//2,自动续租,(如果租约过期,锁就会被释放掉)，续租失败就是上锁失败
	if keepRespChan, err = jobLock.lease.KeepAlive(cancelCtx, leaseId); err != nil {
		goto FAIl
	}

	//3.处理续租的应答协程
	go func() {
		var (
			keepResp *clientv3.LeaseKeepAliveResponse
		)
		for {
			select {
			case keepResp = <-keepRespChan: //自动续租的应答
				if keepResp == nil {
					//说明自动续租被取消掉了
					goto END
				}

			}
		}
	END:
	}()
	//4.穿件事务txn
	txn = jobLock.kv.Txn(context.TODO())
	//锁路径,抢到key就是抢到锁,都去这个路径下面去抢这个值
	lockKey = common.JOB_LOCK_DIR + jobLock.jobName

	//5.事务强锁
	txn.If(clientv3.Compare(clientv3.CreateRevision(lockKey), "=", 0)).
		Then(clientv3.OpPut(lockKey, "", clientv3.WithLease(leaseId))).
		Else(clientv3.OpGet(lockKey))
	//6.提交事务
	if txnResp, err = txn.Commit(); err != nil {
		//提交事务失败,有可能抢到,有可能没抢到
		goto FAIl
	}

	//7.成功返回,失败释放租约,释放锁(回滚)
	if !txnResp.Succeeded { //锁被占用
		err = common.ERR_LOCK_ALREADY_REQUIRED
		goto FAIl
	}

	//走到这,说明上锁成功
	jobLock.leaseId = leaseId
	jobLock.cancelFunc = cancelFunc
	jobLock.isLocked = true

	return
FAIl:
	cancelFunc()                                  //取消自动续租
	jobLock.lease.Revoke(context.TODO(), leaseId) //释放租约,key马上就会被删掉

	return
}

//释放锁
func (jobLock *JobLock) Unlock() {
	if jobLock.isLocked {
		jobLock.cancelFunc()                                  //取消我们程序自动续租的协程
		jobLock.lease.Revoke(context.TODO(), jobLock.leaseId) //释放租约,那么关联的key就会被删除
	}
}
